前一篇介紹了 ClassroomScheduleDao.kt 的 CRUD 操作,很多人可能會好奇:「既然 DAO 已經能抓資料,為什麼 ViewModel 不直接找 DAO,還要多夾一層 Repository?」
簡單來說,兩者的分工就像是:
今天的主角 ClassroomScheduleRepository.kt,正是站在 DAO 與 ViewModel 之間,負責管理所有課表資料流向的關鍵橋樑。
一開始我也不太理解為什麼需要另外寫一個 Repository 來作為資料調度員,而不是直接寫在 ViewModel 裡。不過實際去了解其用意後,我對 現代 Android MVVM 更有概念。
在 Android 官方架構中,讓 Repository 當作中間的資料調度員,主要有三個關鍵原因:
另外,原本我以為,只要看到 ViewModel → Repository → DAO,就代表這個專案已經把資料存取分層做好了。
但實際重新追程式碼後,我發現事情沒有這麼單純:QueryResultViewModel 確實會透過 Repository 取得資料,但部分查詢與篩選邏輯仍然直接寫在 ViewModel 裡,而且有些邏輯和其他地方存在重複。
ClassroomScheduleRepository.kt負責什麼和 Hermes Agent 一起重讀檔案後,我了解 ClassroomScheduleRepository.kt 是負責把 ClassroomScheduleDao的資料操作包裝起來,提供方法給 ViewModel 使用,讓 ViewModel 不需要直接接觸 DAO 或資料庫操作細節。
class ClassroomScheduleRepository(private val dao: ClassroomScheduleDao) {
fun getAllSchedules(): Flow<List<ClassroomSchedule>> = dao.getAll()
}
getAllSchedules()是取得所有教室課表;可能被 QueryResultViewModel 使用,因為是查詢頁的邏輯。suspend fun queryEmptyRooms(
buildingName: String, day: String, timeSlotKey: String, floorQueryValue: String
): List<ClassroomSchedule> {
val schedules = dao.getSchedulesByBuildingAndFloor("$buildingName$floorQueryValue%")
return schedules.filter { schedule ->
getScheduleStatus(schedule, "${day.lowercase()}$timeSlotKey") == "O"
}
}
O 的教室。 但實際上,QueryResultViewModel 內部自己也做了一次空教室過濾。fun getClassroomDetails(classRoomName: String): Flow<ClassroomSchedule?> {
return dao.getClassroomDetails(classRoomName)
}
RoomDetailViewModel 呼叫,用來根據教室名稱取得該教室資料。ViewModel
「提出資料需求」
│
▼
Repository
「協調資料來源」
│
▼
DAO
「執行資料庫操作」
│
▼
Room Database
「實際儲存資料」
這樣的分層可以讓不同元件各自負責不同工作:ViewModel 處理畫面所需的狀態,Repository 負責協調資料來源,DAO 負責資料庫操作,而 Room Database 負責實際儲存資料。
QueryResultViewModel
「接收查詢條件、處理部分篩選邏輯」
↓
Repository
「協調資料存取」
↓
DAO
「執行資料庫操作」
↓
Room Database
「實際儲存資料」
在這裡,QueryResultViewModel 除了接收查詢條件,也負責了一部分空教室的篩選邏輯。
從架構上來看,ViewModel → Repository → DAO 是一條清楚的資料存取路徑。不過重新讀過 RoomRush 後,我發現實際的職責沒有完全切開。
ClassroomScheduleRepository 是 ViewModel 和 DAO 中間的資料層中介。
它持有 ClassroomScheduleDao,並提供 getAllSchedules()、insert()、insertAll()、update()、delete()、findByName()、getClassroomDetails() 等方法給 ViewModel 使用。多數方法只是薄薄包裝 DAO,但這樣可以讓 ViewModel 不直接接觸 DAO。
值得注意的是,Repository 裡也有 queryEmptyRooms(),但目前查詢頁主要是在 QueryResultViewModel 裡自己篩選 emptyRooms,代表查詢邏輯有分散與可能重複的情況。
有些查詢邏輯在 Repository 裡,有些在 QueryResultViewModel 裡,可能有重複和分散的部分。
特別是在, Repository 的 queryEmptyRooms() 用 == "O",而 QueryResultViewModel 用 != "X",兩者邏輯接近但不完全一致,未來可能需要統一。
== "O" 才算可用。!= "X" 就一律視為可用。乍看之下兩者差不多,但可能導致邊界情境(Edge Case):如果某個時段的資料解析出現 null、空白字串或異常代碼:
理想的重構作法是定義明確的狀態標籤(例如 AVAILABLE 代表可用、BUSY 代表佔用),不要再用字串 "O" 和 "X";並把所有過濾規則收斂回 Repository 內,確保整個專案永遠只遵照同一個「單一真實來源」。
讀完 ClassroomScheduleRepository 後,我更清楚 RoomRush 的資料層分工。
DAO 定義了實際的資料庫操作,而 Repository 則把 DAO 包裝成 ViewModel 可以使用的方法。這樣 ViewModel 不需要直接知道 SQL 或 DAO 細節,只要透過 Repository 取得資料。
不過這個檔案也讓我看到一個值得記錄的技術債:Repository 裡有 queryEmptyRooms(),但目前查詢頁的主要空教室篩選邏輯是在 QueryResultViewModel 裡完成。
也就是說,查詢邏輯有一部分分散在不同層。未來如果要讓專案更容易維護,可以思考是否要把查詢規則集中到 Repository,或至少統一「可用教室」的判斷條件。
簡單用一句話總結這個檔案:
ClassroomScheduleRepository包裝 DAO,讓 ViewModel 透過 Repository 取得與操作教室課表資料。
前面我們已經了解 ClassroomScheduleRepository.kt 如何透過 DAO 操作資料。
但下一個問題隨之而來: AppDatabase.kt 到底是怎麼把 SF.csv/ES.csv 裡的原始課表資料,變成 Room Database 裡的 ClassroomSchedule?
下一篇我們會繼續往後追資料來源,把這段看懂之後,就能真正串起「原始 CSV 課表 → Room Database → App 查詢」的資料流。